package mahjong;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.Serializable;
import java.util.*;

/** 
 * This class represents a mahjong board with tiles in it.
 */
public class Board implements Serializable
{
	/** Constant number of tiles in a group. */
	public static final int GROUPSIZE = 4;
	/** Constant number of groups. */
	public static final int MAXGROUPS = 36;
	/** The random variable used to construct the board. */
	private Random r;
	/** An array of all the tiles on the board. */
	private Tile[] tiles;
	/** Dimensions of a layer. */
	private int height;
	/** Dimensions of a layer. */
	private int width;
	/** Number of layers. */
	private int depth;
	/** The current selected tile. */
	private Tile selected;
	/** Hint mode flag */
	private boolean hintMode = false;
	/** Test mode flag */
	private boolean testMode = false;
	/** Record the "correct" moves when constructing the board */
	public LinkedList<Tile> moves;
	/** Undo and redo stack */
	private Stack<Tile> undoStack, redoStack;
	/** Game stated flag */
	private boolean gameStarted = false;
	/** Board layout*/
	private String bName;
	/** flag for Highscore available*/
	private boolean canHighScore = true;
	/** Current game time */
	private int gameTime = 0;
	/** ActionListener to choose move every game tick **/
	private ActionListener aiHandler;
	/** True if the chosen AI move has already been displayed **/
	private boolean moveDisplayed;
	/** Chosen AI Move **/
	private Tile[] aiMove;
	/** True if the AI is currently playing **/
	private boolean autoplay;
	private boolean generateSolvable = true;
	private transient AIPlayer ai;

	/** Construct a new board from a given template file.
	 * 
	 * @param boardName template file name.
	 */
	public Board(String boardName)
	{
		this(null, boardName, true);
	}

	/** Construct a new board from a given template file, but using a given
	 * random variable.
	 * 
	 * @param boardName template file name.
	 * @param r1 the random variable to use.
	 */
	public Board(Random r1, String boardName)
	{
		this(r1, boardName, true);
	}

	public Board(String bName, boolean solvable)
	{
		this(null, bName, solvable);
	}
	
	/** 
	 * Creates a new game with the provided board layout
	 * @param r1 the random seed to used to create the board
	 * @param boardName board layout
	 */
	public void NewBoard(Random r1, String boardName)
	{
		bName = boardName; 
		generateSolvable = true;

		if (boardName.endsWith("test.layout")) {
			testMode = true;
		} else {
			testMode = false;
		}

		r = r1;

		// Construct a Layout object to read the layout
		Layout ly = new Layout(boardName);
		int[][][] levels = ly.getLevels();

		if (levels != null) {
			// initialize all the tiles
			depth = levels.length;
			height = levels[0].length;
			width = levels[0][0].length;
			tiles = new Tile[ly.getNumTiles()];
			for (int i=0; i<ly.getNumTiles(); i++) {
				tiles[i] = new Tile();
			}
			// link them up
			linkTiles(levels);
			// generate solvable layout
			build(r1);
		}
	}

	public Board(Random r1, String boardName, boolean solvable)
	{
		bName = boardName; 
		generateSolvable = solvable;

		if (boardName.endsWith("test.layout")) {
			testMode = true;
		} else {
			testMode = false;
		}

		r = r1;

		// Construct a Layout object to read the layout
		Layout ly = new Layout(boardName);
		int[][][] levels = ly.getLevels();

		if (levels != null) {
			// initialize all the tiles
			depth = levels.length;
			height = levels[0].length;
			width = levels[0][0].length;
			tiles = new Tile[ly.getNumTiles()];
			for (int i=0; i<ly.getNumTiles(); i++) {
				tiles[i] = new Tile();
			}
			// link them up
			linkTiles(levels);
			// generate solvable layout
			build(r1);
		}
	}


	/** Get all the tiles, in play or not.
	 * @return Tile content. */
	public Tile[] getContent()
	{
		return tiles; 
	}

	/** Get the width of the board.
	 * @return board width in tiles. */
	public int getWidth()
	{
		return width;
	}

	/** Get the height of the board.
	 * @return board height in tiles. */
	public int getHeight()
	{
		return height;
	}

	/** Get the depth of the board.
	 * @return board height in tiles. */
	public int getDepth()
	{
		return depth;
	}

	/** Set up the adjacency relation between the tiles given the layout.
	 * @param levels a 3-D array representing the layout, given by the Layout class.
	 * Each tile is represented by a unique ID starting from 1. The ID will be present
	 * in the array positions representing the four corner points of the tile.
	 * @See Layout
	 */
	private void linkTiles(int[][][] levels)
	{
		//
		int curtile = 0;

		for (int z = 0; z < depth; z++) {
			for (int y = 0; y < height; y++) {
				for (int x = 0; x < width; x++) {
					// Check for the next tile
					if (levels[z][y][x] > curtile) {
						Tile tile = tiles[levels[z][y][x]-1];
						tile.setCoord(z, y, x);
						tile.setId(levels[z][y][x] - 1);

						// Link to the adjacent tiles
						if (x-1 >= 0 && levels[z][y][x-1] != 0)
							tile.setAdjacentTile(0, tiles[levels[z][y][x-1]-1]);
						if (x+2 < width && levels[z][y][x+2] != 0)
							tile.setAdjacentTile(1, tiles[levels[z][y][x+2]-1]);
						if (x+2 < width && levels[z][y+1][x+2] != 0)
							tile.setAdjacentTile(2, tiles[levels[z][y+1][x+2]-1]);
						if (x-1 >= 0 && levels[z][y+1][x-1] != 0)
							tile.setAdjacentTile(3, tiles[levels[z][y+1][x-1]-1]); 

						// Link to the tiles above
						if (z+1 < depth && levels[z+1][y][x] != 0)
							tile.setAdjacentTile(4 , tiles[levels[z+1][y][x]-1]);
						if (z+1 < depth && levels[z+1][y][x+1] != 0)
							tile.setAdjacentTile(5, tiles[levels[z+1][y][x+1]-1]);
						if (z+1 < depth && levels[z+1][y+1][x+1] != 0)
							tile.setAdjacentTile(6, tiles[levels[z+1][y+1][x+1]-1]);
						if (z+1 < depth && levels[z+1][y+1][x] != 0)
							tile.setAdjacentTile(7, tiles[levels[z+1][y+1][x]-1]);

						curtile++;
					}
				}
			}
		}
	}

	/** Basic setup routine. Uses or creates a random number generator and
	 * then initializes the board. Generate solvable boards for now.
	 * First, put all the tiles into play and pretend they all match. Randomly
	 * take two exposed tiles, and assign them to a same group. Repeat this process
	 * untill stuck all the board is cleared. If stuck, then retry, the probability
	 * of stucking goes down exponentially in the number of trials.
	 *
	 * @param r1 the random number generator to use.
	 */
	protected void build(Random r1)
	{
		undoStack = new Stack<Tile>();
		redoStack = new Stack<Tile>();

		if (r1 == null) {
			long seed = new Random().nextLong();
			r = new Random(seed);
			//System.out.println("Seed used: "+seed);
		} else {
			r = r1;
		}

		// Test mode code
		if (testMode) {
			tiles[0].setValue(1);
			tiles[0].setSubindex(1);
			tiles[0].addToPlay();
			tiles[1].setValue(1);
			tiles[1].setSubindex(2);
			tiles[1].addToPlay();
			tiles[2].setValue(2);
			tiles[2].setSubindex(3);
			tiles[2].addToPlay();
			tiles[3].setValue(2);
			tiles[3].setSubindex(2);
			tiles[3].addToPlay();
			return;
		}

		// Normal code
		if (generateSolvable) {
			buildSolvable();
		} else {
			buildRandom();
		}
	}

	private void buildSolvable()
	{
		int[] groups = new int[MAXGROUPS * 2];
		int[] subindices = new int[MAXGROUPS];
		for (int i = 0; i < MAXGROUPS; i++) {
			groups[i * 2] = i;
			groups[i * 2 + 1] = i;
			subindices[i] = 0;
		}

		// Shuffle the groups
		for (int i = groups.length - 1; i > 0; i--) {
			int j = r.nextInt(i + 1);
			int k;
			k = groups[j];
			groups[j] = groups[i];
			groups[i] = k;
		}

		// Put the all tiles into play, then play the game
		// randomly assuming each move is valid
		for (int i = 0; i < tiles.length; i++) {
			tiles[i].addToPlay();
			tiles[i].setValue(-1);
			tiles[i].setSubindex(-1);
		}

		moves = new LinkedList<Tile>();
		Tile[] removableTiles = new Tile[tiles.length];
		int numRemovable;
		int m1, m2;

		for (int i = 0; i < groups.length; i++) {
			// find all exposed tiles
			numRemovable = 0;
			for (int j = 0; j < tiles.length; j++) {
				if (tiles[j].canRemove() && tiles[j].getValue() < 0) {
					removableTiles[numRemovable++] = tiles[j];
				}
			}
			if (numRemovable < 2) {
				// less than two tiles available, we are stucked
				//System.out.println("Noooooooooo board not solvable rebuilding");
				build(r);
				return;
			}
			// randomly pick 2 exposed tiles
			m1 = r.nextInt(numRemovable);
			m2 = r.nextInt(numRemovable);
			while (m1 == m2) m2 = r.nextInt(numRemovable);
			// Set them to the same group and remove them
			removableTiles[m1].setValue(groups[i]);
			removableTiles[m2].setValue(groups[i]);
			removableTiles[m1].setSubindex(subindices[groups[i]]++);
			removableTiles[m2].setSubindex(subindices[groups[i]]++);
			removableTiles[m1].removeFromPlay();
			removableTiles[m2].removeFromPlay();
			// remember this move
			moves.add(removableTiles[m1]);
			moves.add(removableTiles[m2]);
		}

		// put everything back, with their groups and subgroups randomly set.
		for (int i = 0; i < tiles.length; i++) {
			tiles[i].addToPlay();
		}
	}

	private void buildRandom()
	{
		int[] groups = new int[MAXGROUPS * 4];
		int[] subindices = new int[MAXGROUPS];
		for (int i = 0; i < MAXGROUPS; i++) {
			groups[i * 4] = i;
			groups[i * 4 + 1] = i;
			groups[i * 4 + 2] = i;
			groups[i * 4 + 3] = i;
			subindices[i] = 0;
		}

		// Shuffle the groups
		for (int i = groups.length - 1; i > 0; i--) {
			int j = r.nextInt(i + 1);
			int k;
			k = groups[j];
			groups[j] = groups[i];
			groups[i] = k;
		}

		for (int i = 0; i < tiles.length; i++) {
			tiles[i].setValue(groups[i]);
			tiles[i].setSubindex(subindices[groups[i]]++);
		}
	}

	/**
	 * Wipes the current board and constructs a new one using the current
	 * Random variable.
	 * Used exclusively for testing
	 */
	public void reset()
	{
		hint(false);		
		if (selected != null)
			selected.setSelected(false);
		selected = null;
		if(aiMove != null)
		{
			aiMove[0].aiSelected(false);
			aiMove[1].aiSelected(false);
		}
		aiMove = null;
		if(aiHandler != null) GameTime.timer.removeActionListener(aiHandler);
		aiHandler = null;
		autoplay = false;
		moveDisplayed = false;
		build(r);
	}
	
	/**
	 * Wipes the board and creates a new board with the provided layout option
	 * @param i layout option 0 = turtle 1 = ziggurat 
	 */
	public void resetGame(int i)
	{
		hint(false);		
		if (selected != null)
			selected.setSelected(false);
		selected = null;
		if(aiMove != null)
		{
			aiMove[0].aiSelected(false);
			aiMove[1].aiSelected(false);
		}
		aiMove = null;
		if(aiHandler != null) GameTime.timer.removeActionListener(aiHandler);
		aiHandler = null;
		autoplay = false;
		moveDisplayed = false;
		if(i == 0)
		{
			NewBoard(r, "Layouts"+ java.io.File.separator+"turtle.layout");
		}
		else
		{
			NewBoard(r, "Layouts"+ java.io.File.separator+"ziggurat.layout");
		}
	}

	/** 
	 * Check if the board is empty or not. 
	 * @return true if the board has no more tiles, false otherwise. */
	public boolean isEmpty()
	{
		for (int i = 0; i < tiles.length; i++) {
			if(tiles[i].isRemoved() != true) return false;
		}

		return true;
	}

	/** Attempt to remove the given tile; the tile must be present. 
	 * @param t the tile to remove.
	 * @return false if the tile cannot be removed, true otherwise. */
	public boolean remove(Tile t)
	{
		if (!t.canRemove())
			return false;

		t.removeFromPlay();
		return true;
	}

	/** Detect if a the tile configuration is a deadlock.
	 * @return true if the board is non empty and no two exposed tiles
	 * match, false otherwise.
	 */
	public boolean isDeadLock()
	{
		if (this.isEmpty()) return false;

		for (int i = 0; i<tiles.length; i++) {
			for (int j=i+1; j<tiles.length; j++) {
				if (!tiles[i].isRemoved() && !tiles[j].isRemoved() && tiles[i].canRemove()
						&& tiles[j].canRemove() && tiles[i].matches(tiles[j]))
					return false;
			}
		}

		return true;
	}

	/** Make a move on a tile.
	 * If the given tile is not exposed, do nothing. Else if the given tile
	 * is already selected, it becomes unselected. If no other tile
	 * is currently selected, select the given tile. If another tile is
	 * selected and it matches the given tile, both tiles are removed from
	 * the board and no tile is selected. If they don't match, the given
	 * tile become selected and replace the old selected tile.
	 *
	 * @param t the tile on which the move is made.
	 */
	public void makeMove(Tile t)
	{
		if (t.canRemove() && !gameStarted) gameStarted = true;
		if (selected == null) {
			if (!t.isRemoved() && t.canRemove()) {
				t.setSelected(true);
				selected = t;
			}
		} else {
			if (t != selected && t.matches(selected) && t.canRemove()) {
				if (!redoStack.empty()) {
					redoStack = new Stack<Tile>();
				}
				undoStack.push(t);
				undoStack.push(selected);
				t.setSelected(false);
				t.removeFromPlay();
				selected.setSelected(false);
				selected.removeFromPlay();
				selected = null;
				if (hintMode) hint(true);
			} else {
				if (t != selected && t.canRemove()) {
					selected.setSelected(false);
					selected = t;
					selected.setSelected(true);
				} else if (t == selected) {
					selected.setSelected(false);
					selected = null;
				}
			}
		}
	}
	


	/** Hint mode toggle.
	 * @param drawHint whether turn on hint mode or not.
	 */
	public void hint(boolean drawHint)
	{
		hintMode = drawHint;
		
		for(int i = 0; i < tiles.length; i++)
		{
			tiles[i].setHint(0);
		}
		
		if(drawHint)
		{
			int[] groups = new int[MAXGROUPS]; 
			
			for(int i = 0; i < tiles.length; i++)
			{
				if(!tiles[i].isRemoved() && tiles[i].canRemove()) groups[tiles[i].getValue()]++;
			}
			
			
			for(int i = 0; i<MAXGROUPS; i++)
			{
				if(groups[i] == 4)
				{
					for(int j = 0; j < tiles.length; j++)
					{
						if(tiles[j].getValue() == i) tiles[j].setHint(2);
					}
				}else if(groups[i] >= 2)
				{
					for(int j = 0; j < tiles.length; j++)
					{
						if(tiles[j].getValue() == i && tiles[j].canRemove()) tiles[j].setHint(1);
					}
				}
			}			
		}
	}

	/** Auto play the game, for testing only */
	public boolean autoPlay(int aikind)
	{
		if (selected != null) {
			selected.setSelected(false);
			selected = null;
		}
		
		if (aikind == 0) {
			ai = new MonteCarloPlayer();
		} else if (aikind == 1) {
			ai = new HeuristicPlayer();
		} else if (aikind == 2) {
			ai = new CheatPlayer();
		} else {
			ai = new RandomPlayer();
		}

		ai.init(this);

		while (!isEmpty() && !isDeadLock()) 
		{
			Tile[] move= ai.chooseMove();
			makeMove(move[0]);
			makeMove(move[1]);
		}

		return isEmpty(); 
	}
	
	/**
	 * Tests if the boatd is solvable
	 */

	public void testSolvability()
	{
		while (!isEmpty() && !isDeadLock()) {
			Tile t1 = moves.pop();
			Tile t2 = moves.pop();
			makeMove(t1);
			makeMove(t2);
		}
	}
	/**
	 * Start an AI player
	 * @param v Viewer that holds the baord
	 * @param aikind int denoting AI kind
	 */
	public void startAutoPlay(final Viewer v, int aikind)
	{
		if (selected != null) {
			selected.setSelected(false);
			selected = null;
		}
		
		autoplay = true;
		
		if (aikind == 0) {
			ai = new MonteCarloPlayer();
		} else if (aikind == 1) {
			ai = new HeuristicPlayer();
		} else if (aikind == 2) {
			ai = new CheatPlayer();
		} else {
			ai = new RandomPlayer();
		}
		
		ai.init(this);

		aiHandler = new ActionListener() {
			public void actionPerformed(ActionEvent e) 
			{	
				if (!isEmpty() && !isDeadLock()) 
				{
					if(moveDisplayed == true)
					{
						moveDisplayed = false;
						aiMove[0].aiSelected(false);
						aiMove[1].aiSelected(false);
						makeMove(aiMove[0]);
						makeMove(aiMove[1]);
					}
					else
					{
						aiMove = ai.chooseMove();
						aiMove[0].aiSelected(true);
						aiMove[1].aiSelected(true);
						moveDisplayed = true;
					}
					
					v.repaint();
				}
			}
		};
		GameTime.timer.addActionListener(aiHandler);
		GameTime.startTimer();
	}
	
	/**
	 * Stop all AI players from playing
	 */
	public void stopAutoPlay()
	{
		if(aiMove != null)
		{
			aiMove[0].aiSelected(false);
			aiMove[1].aiSelected(false);
		}
		aiMove = null;
		moveDisplayed = false;
		autoplay = false;
		
		
		if(aiHandler != null) GameTime.timer.removeActionListener(aiHandler);
		aiHandler = null;
	}

	/** Undo last move that removed a pair of tiles */
	public void undo()
	{
		Tile t;

		if (undoStack.empty()) return;

		if (selected != null) {
			selected.setSelected(false);
			selected = null;
		}

		t = undoStack.pop();
		t.addToPlay();
		redoStack.push(t);

		t = undoStack.pop();
		t.addToPlay();
		redoStack.push(t);
		
		if(hintMode) hint(true);
			
	}

	/** Redo last undo */
	public void redo()
	{
		Tile t;

		if (redoStack.empty()) return;

		if (selected != null) {
			selected.setSelected(false);
			selected = null;
		}

		t = redoStack.pop();
		t.removeFromPlay();
		//makeMove(t);
		undoStack.push(t);

		t = redoStack.pop();
		t.removeFromPlay();
		//makeMove(t);
		undoStack.push(t);
		
		if(hintMode) hint(true);
	}
	
	/** Load game.
	 * @param board the board to be loaded
	 */
	public void load(Board board)
	{
		tiles = board.getContent();
		height = board.getHeight();
		width = board.getWidth();
		depth = board.getDepth();
		gameTime = board.getGameTime();
		hintMode = board.hintMode;
		moves = board.moves;
		undoStack = board.undoStack;
		redoStack = board.redoStack;
		gameStarted = board.gameStarted;
	}
	/**
	 * Return which layout the board uses
	 * @return board layout name
	 */
	/** Get the name of the current layout.
	 * @return the name of the current layout.
	 */
	public String getBName()
	{
		return bName;
	}
	
	/**
	 * Saves the game time to the board prior to serialization
	 * @param t time played
	 */
	public void saveGameTime(int t)
	{
		gameTime = t;
	}

	/**
	 * Return the time played
	 * @return the game time in int value
	 */
	/** Get game time. */
	public int getGameTime()
	{
		return gameTime;
	}

	/**
	 * Set the state of the HighScore mode
	 * @param b set true if HighScore available false otherwise
	 */
	public void setHS(boolean b)
	{
		this.canHighScore = b;
	}
	
	/** Check if the move stack is empty or not
	 * @return true if move stack is empty
	 *  */
	public boolean isUndoEmpty()
	{
		return undoStack.isEmpty();
	}
	/**
	 * Queries the board and returns all pairs that can be matched
	 * @return ArrayList<Tile[]> of 2x1 tile arrays representing moves
	 */
	public ArrayList<Tile[]> getAvailableMoves()
	{
		ArrayList<Tile> aTiles = new ArrayList<Tile>();
		ArrayList<Tile[]> moves = new ArrayList<Tile[]>();
		
		// find all exposed tiles
		for (int i = 0; i < tiles.length; i++) {
			if (tiles[i].canRemove()) {
				aTiles.add(tiles[i]);
			}
		}

		// go through the array and find matching pairs
		for(int i = 0; i < aTiles.size(); i++) {
			for(int j = i + 1; j < aTiles.size(); j++) {
				if(aTiles.get(i).getValue() == aTiles.get(j).getValue()) {
					moves.add(new Tile[]{aTiles.get(i), aTiles.get(j)});
				}
			}
		}		
		return moves;		
	}
	/**
	 * Returns the state of the ai play
	 * @return boolean true if ai is on
	 */
	public boolean aiPlaying()
	{
		return autoplay;
	}
}
